Skip to content

Conversation

@ajtmccarty
Copy link
Contributor

@ajtmccarty ajtmccarty commented Aug 12, 2025

part of fixing opsmill/infrahub#7022

handles the situation when a None is compared to a non-None. for example {"name": None} and {"name": {"value": None}} should be merged as {"name": {"value": None}}

Summary by CodeRabbit

  • Bug Fixes

    • Improved deep-merge behavior when combining values with None, ensuring non-None values are preserved and equal values are respected.
    • Clarified conflict handling for mismatched non-None values to prevent ambiguous merges.
    • Ensured list merging remains consistent while avoiding duplicates.
  • Tests

    • Added unit tests covering None-vs-non-None merge scenarios to validate expected outcomes and prevent regressions.

@ajtmccarty ajtmccarty requested a review from a team August 12, 2025 21:38
@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

Walkthrough

Updated deep_merge_dict in infrahub_sdk/utils.py to adjust merge semantics, especially for None handling and equality checks. Tests extended to cover merging dicts where one side has None values to ensure expected outcomes. No public API signatures changed.

Changes

Cohort / File(s) Summary
Deep merge behavior
infrahub_sdk/utils.py
Refactored deep_merge_dict to use local variables per key; added explicit None handling (prefer non-None); adjusted equality/None post-checks; preserved conflict error for unequal non-None values; updated list merge to use local vars.
Unit tests for deep merge
tests/unit/sdk/test_utils.py
Added test case validating dict merge when one side provides None and the other non-None; asserts merged result prefers non-None values.

Sequence Diagram(s)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Assessment against linked issues

Objective Addressed Explanation
Fix conflict when display labels mix attribute and property forms (name vs name__value) [#7022] Changes focus on None vs non-None merging; unclear if this resolves conflicts for differing non-None values in display label resolution.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Added None-preference behavior in deep_merge_dict (infrahub_sdk/utils.py) The linked issue concerns conflicts between attribute vs property forms; it’s unclear if None handling is directly required. Unsure if this is out of scope without the calling context.
New unit test for None-handling in dict merge (tests/unit/sdk/test_utils.py) Test targets None merge semantics, not explicitly tied to the attribute/property conflict scenario described in #7022. Unsure if necessary for the issue.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ajtm-08122025-display-label-def

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or Summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov
Copy link

codecov bot commented Aug 12, 2025

Codecov Report

❌ Patch coverage is 88.88889% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/utils.py 88.88% 0 Missing and 1 partial ⚠️
@@            Coverage Diff             @@
##           stable     #494      +/-   ##
==========================================
- Coverage   75.83%   75.76%   -0.08%     
==========================================
  Files         100      100              
  Lines        8870     8825      -45     
  Branches     1740     1728      -12     
==========================================
- Hits         6727     6686      -41     
+ Misses       1667     1664       -3     
+ Partials      476      475       -1     
Flag Coverage Δ
integration-tests 34.70% <0.00%> (-0.08%) ⬇️
python-3.10 48.14% <88.88%> (-0.25%) ⬇️
python-3.11 48.16% <88.88%> (-0.22%) ⬇️
python-3.12 48.14% <88.88%> (-0.22%) ⬇️
python-3.13 48.12% <88.88%> (-0.22%) ⬇️
python-3.9 46.81% <88.88%> (-0.17%) ⬇️
python-filler-3.12 25.15% <0.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/utils.py 85.38% <88.88%> (+0.27%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
infrahub_sdk/utils.py (5)

151-153: List merge is correct but O(n^2); consider a fast path for empty lists.

Current membership checks make this quadratic. A small readability/perf win: early-return when either side is empty.

Apply this diff to short-circuit obvious cases:

-            elif isinstance(a_val, list) and isinstance(b_val, list):
+            elif isinstance(a_val, list) and isinstance(b_val, list):
                 # Merge lists
                 # Cannot use compare_list because list of dicts won't work (dict not hashable)
-                dicta[key] = [i for i in a_val if i not in b_val] + b_val
+                if not a_val:
+                    dicta[key] = b_val
+                elif not b_val:
+                    dicta[key] = a_val
+                else:
+                    dicta[key] = [i for i in a_val if i not in b_val] + b_val

154-157: None-aware merge resolves the “plain vs property” conflict (issue #7022).

  • If a_val is None and b_val is not None → take b_val.
  • If b_val is None → keep a_val.
  • Equal scalars → no-op.
    This exactly handles cases like {"name": None} + {"name": {"value": None}} → {"name": {"value": None}}.

Consider documenting this behavior in the function docstring for future readers.


139-141: Document the new None-handling semantics in the docstring.

Clarify merge rules to prevent regressions and ease maintenance.

Apply this diff to enhance the docstring:

 def deep_merge_dict(dicta: dict, dictb: dict, path: list | None = None) -> dict:
-    """Deep Merge Dictionary B into Dictionary A.
-    Code is inspired by https://stackoverflow.com/a/7205107
-    """
+    """Deep merge Dictionary B into Dictionary A (mutates A).
+    - Dicts are merged recursively.
+    - Lists are merged by appending unique items from A that are not in B, preserving B's order.
+    - None-handling:
+      * If A has None and B has non-None, take B's value.
+      * If B has None and A has non-None, keep A's value.
+      * If values are equal, keep A.
+    - For differing non-None, non-mergeable scalars, a ValueError is raised (“Conflict at <path>”).
+    Code is inspired by https://stackoverflow.com/a/7205107
+    """

144-147: Minor readability improvement: iterate items to avoid repeated dict lookups.

Using items() removes double indexing and keeps b_val scoped consistently.

Apply this diff:

-    for key in dictb:
-        b_val = dictb[key]
+    for key, b_val in dictb.items():
         if key in dicta:
-            a_val = dicta[key]
+            a_val = dicta[key]

160-161: Use the local b_val for consistency and a tiny perf win.

Avoids re-indexing dictb.

Apply this diff:

-        else:
-            dicta[key] = dictb[key]
+        else:
+            dicta[key] = b_val
tests/unit/sdk/test_utils.py (1)

99-105: Good coverage for scalar None-vs-non-None; add nested dict vs None to reflect the reported issue.

To directly exercise the “name” vs “name__value” case that motivated this PR, add tests where one side is a dict and the other is None.

Apply this diff within the same test or as separate assertions:

     assert deep_merge_dict(d, e) == {"keyA": [10, 20, 30], "keyB": "foo"}
     assert deep_merge_dict(f, g) == {"keyA": "foo", "keyB": "bar"}
+
+    # None vs dict handling (motivating scenario)
+    h = {"name": None}
+    i = {"name": {"value": None}}
+    assert deep_merge_dict(h, i) == {"name": {"value": None}}
+    # Reverse direction should preserve the non-None dict
+    assert deep_merge_dict(i, h) == {"name": {"value": None}}

Alternatively, you can add a dedicated test function:

def test_deep_merge_dict_none_vs_dict():
    assert deep_merge_dict({"name": None}, {"name": {"value": None}}) == {"name": {"value": None}}
    assert deep_merge_dict({"name": {"value": None}}, {"name": None}) == {"name": {"value": None}}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5daba0e and d5ac4ab.

📒 Files selected for processing (2)
  • infrahub_sdk/utils.py (1 hunks)
  • tests/unit/sdk/test_utils.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/unit/sdk/test_utils.py (1)
infrahub_sdk/utils.py (1)
  • deep_merge_dict (138-162)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: unit-tests (3.9)
  • GitHub Check: unit-tests (3.13)
  • GitHub Check: unit-tests (3.12)
  • GitHub Check: unit-tests (3.10)
  • GitHub Check: unit-tests (3.11)
  • GitHub Check: integration-tests-latest-infrahub
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
infrahub_sdk/utils.py (1)

148-150: Recursion on dict values is correct and preserves in-place merge semantics.

Using a_val/b_val locals improves readability and avoids repeated lookups. This aligns with the desired deep-merge behavior.

Copy link
Contributor

@ogenstad ogenstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I'm wondering if there's a point to have deep_merge_dict() in the SDK, to me that feels wrong as it's only used within Infrahub. Not really related to this PR though.

@ajtmccarty ajtmccarty merged commit 3af168c into stable Aug 13, 2025
20 checks passed
@ajtmccarty ajtmccarty deleted the ajtm-08122025-display-label-def branch August 13, 2025 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants